function PTZScene(id, origin, panRange, tiltRange, zoomRange, width, height, totalPanDegrees, totalTiltDegrees){
	this.id = id;
	this.origin = origin;
	this.panRange = panRange;
	this.tiltRange = tiltRange;
	this.zoomRange = zoomRange;
	this.width = width;
	this.height = height;
	this.totalPanDegrees = totalPanDegrees;
	this.totalTiltDegrees = totalTiltDegrees;
	
	this.x = new Array();
	this.y = new Array();
	this.z = new Array();
	this.u = new Array();
	this.v = new Array();
	this.panNormals = new Array();
	this.tiltNormals = new Array();
	this.zoomNormals = new Array();
	this.indices = new Array();
}

// adds a correspondence to the scene, and automatically converts lat/lon/alt into
// XYZ, as well as normalizing the pan/tilt/zoom values.
PTZScene.prototype.addCorrespondence = function(id, lat,lon,alt,u,v,pan,tilt,zoom){
	var i = this.x.length;
	this.panNormals[i]  = (pan - this.panRange[0]) / (this.panRange[1] - this.panRange[0]);
	this.tiltNormals[i] = (tilt - this.tiltRange[0]) / (this.tiltRange[1] - this.tiltRange[0]);
	this.zoomNormals[i] = (zoom - this.zoomRange[0]) / (this.zoomRange[1] - this.zoomRange[0]);
	
	var metersPerLat = distance(this.origin.lat, this.origin.lon, 0, this.origin.lat+0.00001, this.origin.lon, 0) / 0.00001;
	var metersPerLon = distance(this.origin.lat, this.origin.lon, 0, this.origin.lat, this.origin.lon+0.00001, 0) / 0.00001;
	this.x[i] = (lon - this.origin.lon) * metersPerLon;
	this.y[i] = (lat - this.origin.lat) * metersPerLat;
	this.z[i] = (alt - this.origin.alt);
	
	this.u[i] = u;
	this.v[i] = v;
	this.indices[i] = id;
}

PTZScene.prototype.calibrate = function(iterations, epsilon, monitor){
	const toRadians = Math.PI / 180;
	var n = this.x.length;
	// calculate the mean.
	var sumX = 0;
	var sumY = 0;
	var sumZ = 0;
	for (var i = 0; i < n; i++){
		sumX = sumX + this.x[i];
		sumY = sumY + this.y[i];
		sumZ = sumZ + this.z[i];
	}
	var meanX = sumX / n;
	var meanY = sumY / n;
	var meanZ = sumZ / n;
	
	// calculate the standard deviation.
	var stdSumX = 0;
	var stdSumY = 0;
	var stdSumZ = 0;
	for (var i = 0; i < n; i++){
		var valX = (this.x[i] - meanX);
		var valY = (this.y[i] - meanY);
		var valZ = (this.z[i] - meanZ);
		stdSumX = stdSumX + valX * valX;
		stdSumY = stdSumY + valY * valY;
		stdSumZ = stdSumZ + valZ * valZ;
	}
	var stdX = Math.sqrt(stdSumX / n);
	var stdY = Math.sqrt(stdSumY / n);
	var stdZ = Math.sqrt(stdSumZ / n);
	
	// initial guess and stepsizes for calibration step.
	// camera location is at the mean location +- 1/4 a standard deviation.
	// camera orientation is looking north with the up vector straight up.
	// min/max focal lengths are once and twice the width of the image.
	// parameters to solve are in order:
	//   0-2: camera position (tx, ty, tz)
	//   3-4: camera pan and tilt offset
	//   5-7: focal function parameters (f(z) = f1 + f2 * z + (f3 * z^2)/2)
	//   8-9: total pan and tilt ranges.
	var initial = new Array(sumX / n, sumY / n, sumZ / n, 
							0, 0, 
							this.width/2, this.width * 2, 0,
							this.totalPanDegrees * toRadians, this.totalTiltDegrees * toRadians);
	alert(initial)
	var stepSizes = new Array(stdX, stdY, stdZ,
							 Math.PI / 8, Math.PI / 16,
							 100, 100, 20,
							 Math.PI / 16, Math.PI / 16)
    alert(stepSizes);
	
	var me = this;
	
	/* Angular Error: */
	var errFun = function(sol){
		var e = ptzAngleError(me.x, me.y, me.z, me.u, me.v,
								   me.panNormals, me.tiltNormals, me.zoomNormals,
								   me.width,me.height,sol);
		
		return e;
		
	};
	
	
	var s = new Simplex(errFun, initial, stepSizes);
	var solution = s.minimize(epsilon * n, iterations, true);
	
	
	solution[8] = 320.0 * Math.PI / 180.0;
	solution[9] = 120.0 * Math.PI / 180.0;
	
	
	// now that we've solved for the position and orientation of the camera,
	// use projection error to solve for focal lengths more precisely.
	
	var focalError = function(sol){
		var mySol = new Array(solution[0], solution[1], solution[2], 
							  solution[3], solution[4], 
							  sol[0], sol[1],
							  solution[7], solution[8])

		var e = ptzAngleError(me.x, me.y, me.z, me.u, me.v,
								   me.panNormals, me.tiltNormals, me.zoomNormals,
								   me.width,me.height,mySol);		
		return e;
		
	};
	
	var focalInitial = new Array(this.width/2, this.width * 2, 0);
	var focalSteps = new Array(10, 10, 2);
	var focalSimplex = new Simplex(focalError, focalInitial, focalSteps);
	var focalSolution = focalSimplex.minimize(epsilon * n, iterations, false);
	
	solution[5] = focalSolution[0];
	solution[6] = focalSolution[1];
	solution[7] = focalSolution[2];
	
	
	if (monitor){
		this.displaySolution(solution);
	}
	this.submitSolution(solution, monitor);
	return {error: errFun(solution), solution: solution};
}

PTZScene.prototype.submitSolution = function(solution, monitor){
	var latsPerMeter = 0.00001 / distance(this.origin.lat, this.origin.lon, 0, this.origin.lat+0.00001, this.origin.lon, 0);
	var lonsPerMeter = 0.00001 / distance(this.origin.lat, this.origin.lon, 0, this.origin.lat, this.origin.lon+0.00001, 0);
	var cameraLon = solution[0] * lonsPerMeter + this.origin.lon;
	var cameraLat = solution[1] * latsPerMeter + this.origin.lat;
	var cameraAlt = solution[2] + this.origin.alt;
	var panOffset         = solution[3];
	var tiltOffset        = solution[4];
	var rollOffset        = 0;
	var focalF1          = solution[5];
	var focalF2          = solution[6];
	var focalF3          = solution[7];
	//var totalPanDegrees       = solution[7] * 180.0 / Math.PI;
	//var totalTiltDegrees      = solution[8] * 180.0 / Math.PI;
	var totalPanDegrees = 320;
	var totalTiltDegrees = 120;
	
	var params = '&scene=' + this.id;
	params = params + '&originLat=' + this.origin.lat + '&originLon=' + this.origin.lon + '&originAlt=' + this.origin.alt;
	params = params + '&tx=' + solution[0] + '&ty=' + solution[1] + '&tz=' + solution[2];
	params = params + '&cameraLat=' + cameraLat + '&cameraLon=' + cameraLon + '&cameraAlt=' + cameraAlt;
	params = params + '&panOffsetRad=' + panOffset + '&tiltOffsetRad=' + tiltOffset + '&rollOffsetRad=' + rollOffset;
	params = params + '&focalLengthF1=' + focalF1 + '&focalLengthF2=' + focalF2 + '&focalLengthF3=' + focalF3;
	params = params + '&totalPanDegrees=' + totalPanDegrees + '&totalTiltDegrees=' + totalTiltDegrees;

	var http = createRequest();
	var url = 'setCalibration';
	var me = this;
	http.open("POST", url, true);
	
	http.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
	http.setRequestHeader("Content-length", params.length);
	http.setRequestHeader("Connection", "close");	
	
	http.onreadystatechange = function(){
		if(http.readyState == 4){
			if (!monitor){
				window.location = "viewScene?scene=" + me.id;
			}
		}
	};
	http.send(params);
}

PTZScene.prototype.displaySolution = function(solution){
	var latsPerMeter = 0.00001 / distance(this.origin.lat, this.origin.lon, 0, this.origin.lat+0.00001, this.origin.lon, 0);
	var lonsPerMeter = 0.00001 / distance(this.origin.lat, this.origin.lon, 0, this.origin.lat, this.origin.lon+0.00001, 0);
	var cameraLon = solution[0] * lonsPerMeter + this.origin.lon;
	var cameraLat = solution[1] * latsPerMeter + this.origin.lat;
	var cameraAlt = solution[2] + this.origin.alt;
	var panOffset         = solution[3];
	var tiltOffset        = solution[4];
	//rollOffset        = sol[5]
	var rollOffset        = 0;
	var focalF1          = solution[5];
	var focalF2          = solution[6];
	var focalF3          = solution[7];
	var totalPanRad       = solution[8];
	var totalTiltRad      = solution[9];
	
	var resultDiv = document.getElementById('result');
	resultDiv.innerHTML =  'Location: (' + cameraLat + ',' + cameraLon + ',' + cameraAlt + ')<br/>'
	resultDiv.innerHTML = resultDiv.innerHTML + 'Rotation: (' + panOffset + ',' + tiltOffset+ ',' + rollOffset + ')<br/>'
	resultDiv.innerHTML = resultDiv.innerHTML + 'Focal: (' + focalF1 + ',' + focalF2 + ',' + focalF3 + ')<br/>'
	resultDiv.innerHTML = resultDiv.innerHTML + 'total pan/tilt: (' + totalPanRad + ',' + totalTiltRad + ')<br/>'
	resultDiv.innerHTML = resultDiv.innerHTML + this.x.length + ' correspondences total.<br/>'

	
	this.dumpIndividualErrors(solution);
	// for wichita
	//var distFromGroundTruth = distance(cameraLat, cameraLon, cameraAlt,  37.686780, -97.340425, 458.0);
	// for montana
	//var distFromGroundTruth = distance(cameraLat, cameraLon, cameraAlt,   46.861205, -113.988970, 1000.0);
	
	//resultDiv.innerHTML = resultDiv.innerHTML + 'estimated location ' + distFromGroundTruth + ' meters away from ground truth.'
}

PTZScene.prototype.dumpIndividualErrors = function(solution){
	var allM = makeManyProjectionMatrices(this.panNormals,this.tiltNormals,this.zoomNormals, 
										  this.width,this.height,   
										  solution[0],solution[1],solution[2],   
										  solution[3],solution[4],0, 
										  solution[8],solution[9], 
										  solution[5],solution[6],solution[7]);
	
	var n = this.x.length;
	var sErr = 0;
	var sErrRad = 0;
	var sErrDeg = 0;
	
	var errTable = '<table><tr><th>ID</th><th>Error [0,2] </th><th>Error in Radians</th><th>Error in Degrees</th></tr>'
	for (var i = 0; i < n; i++){
		var M = allM[i];
		errTable = errTable + '<tr><td>' + this.indices[i] + '</td>' 
		var error = getAngularErrorForCorrespondence(M, solution[0], solution[1], solution[2],
													 this.u[i], this.v[i], this.x[i], this.y[i], this.z[i]);
		errTable = errTable + '<td>' + error + '</td>'
		var errRad = Math.acos(1-error);
		var errDeg = 180.0 / Math.PI * errRad;
		
		errTable = errTable + '<td>' + errRad + '</td>'
		errTable = errTable + '<td>' + errDeg + '</td></tr>'
		
		sErr = sErr + error;
		sErrRad = sErrRad + errRad;
		sErrDeg = sErrDeg + errDeg;
	}
	errTable = errTable + '<tr><td>Sum</td><td>' + sErr + '</td><td>' + sErrRad + '</td><td>' + sErrDeg + '</td></tr>'
	errTable = errTable + '<tr><td>Average</td><td>' + (sErr/n) + '</td><td>' + (sErrRad/n) + '</td><td>' + (sErrDeg/n) + '</td></tr>'
	errTable = errTable + '</table>'
}

function makeProjectionMatrix(panRad, tiltRad, zoom, width, height,
							  tx, ty, tz, panOffset, tiltOffset, rollOffset, focalF1, focalF2, focalF3){
	
	// K is the intrinsic camera matrix.
	var c = zoom;
	var focal = focalF1 + focalF2 * c + 0.5 * (focalF3 * c * c);
	var aspectRatio = (1.0 * width) / (1.0 * height);
	var K = new Matrix(3,3,[[focal * aspectRatio, 0, width / 2],
						[0, -focal, height / 2],
						[0, 0, 1]]);

	// R is the rotation matrix.
	var alpha = -(panRad + panOffset);
	var beta = tiltRad + tiltOffset;
	var gamma = rollOffset + Math.PI;
	
	var ca = Math.cos(alpha)
	var cb = Math.cos(beta)
	var cg = Math.cos(gamma)
	var sa = Math.sin(alpha)
	var sb = Math.sin(beta)
	var sg = Math.sin(gamma)
	
	var R = new Matrix(3,3, [[ca * cg - cb * sa * sg,  cg * sa + ca * cb * sg, sb * sg],
							[-cb * cg * sa - ca * sg, ca * cb * cg - sa*sg,   cg * sb],
							[sa * sb,                 -ca * sb,               cb]])
	// T is the translation vector.
	var center = new Matrix(1,3,[[tx],[ty],[tz]])
	var T = R.multiply(center);
	
	R = R.values;
	T = T.values;
	
	// M is the full projection matrix.
	var tmp = new Matrix(4,3, [[R[0][0], R[0][1], R[0][2], -T[0][0]],
							 [R[1][0], R[1][1], R[1][2], -T[1][0]],
							 [R[2][0], R[2][1], R[2][2], -T[2][0]]]);
	var M = K.multiply(tmp);
	
	return M;
}

function makeManyProjectionMatrices(panNormals, tiltNormals, zoomNormals, width, height, 
									tx, ty, tz, panOffset, tiltOffset, rollOffset, totalPanRad, totalTiltRad,
									focalF1, focalF2, focalF3){
	// Input: intrinsic and extrinsic parameters of a camera across many pan,tilt and zoom configurations.
	// Output: the 3x4 projection matrices that satisfy these parameters.
	var allMatrices = new Array();
	var n = panNormals.length;
	for (var i = 0; i < n; i++){
		var panRad = panNormals[i] * totalPanRad;
		var tiltRad = tiltNormals[i] * totalTiltRad;
		var zoom = zoomNormals[i];
		var mat = makeProjectionMatrix(panRad, tiltRad, zoom, width, height, tx, ty, tz, panOffset, tiltOffset, rollOffset, focalF1, focalF2, focalF3)
		alert('ptz coordinates (' + panNormals[i] + ',' + tiltNormals[i] + ',' + zoomNormals[i] + ') -> (' + panRad + ',' + tiltRad + ',' + zoom + ') have matrix \n' +
		      mat.values[0][0] +',' +mat.values[0][1] +',' +mat.values[0][2] +',' +mat.values[0][3] +';\n' +
		      mat.values[1][0] +',' +mat.values[1][1] +',' +mat.values[1][2] +',' +mat.values[1][3] +';\n' +
		      mat.values[2][0] +',' +mat.values[2][1] +',' +mat.values[2][2] +',' +mat.values[2][3] +';\n' )
		allMatrices[i] = mat;
	}
	return allMatrices;
}

function ptzAngleError(x,y,z,u,v,panNormals,tiltNormals,zoomNormals,width,height,sol){
//	Returns the sum of all angular errors.
//Input: x,y,z                              : positions in ENU space
//	u,v                                : positions in image coordinates
//	panNormals,tiltNormals,zoomNormals : pan, tilt, and zoom, in [0,1].
//	width, height                      : the width and height of the image.
//	sol                                : a 10-element solution vector, where
//	sol[0:2]=tx,ty,tz               :    the position of the camera in ENU space.
//	sol[3:5]=pan0,tilt0,roll0       :    the rotational offsets of the camera.
//	sol[6:8]=focalF1, focalF2, focalF3     :  Taylor series approximation for focal function f(z) = f1 + f2 * z + (f3 * z^2)/2
//	sol[8:9]=dpan, dtilt            :    the total field of view of the camera in pan and tilt.
//Output:  the angular difference error.
	
	var n = x.length;
	var tx                = sol[0];
	var ty                = sol[1];
	var tz                = sol[2];
	var panOffset         = sol[3];
	var tiltOffset        = sol[4];
	//rollOffset        = sol[5]
	var rollOffset        = 0;
	var focalF1          = sol[5];
	var focalF2          = sol[6];
	var focalF3          = sol[7];
	//var totalPanRad       = sol[7];
	//var totalTiltRad      = sol[8];
	var totalPanRad = 320.0 / 180.0 * Math.PI;
	var totalTiltRad = 120.0 / 180.0 * Math.PI;
	
	if (totalPanRad < 0 || totalTiltRad < 0 || totalPanRad > 2 * Math.PI || totalTiltRad > 2 * Math.PI)
		return Infinity;
	
	//if (totalPanRad < Math.PI / 2)
	//	return 10 * (1 - 2 / Math.PI * totalPanRad);

	var allM = makeManyProjectionMatrices(panNormals,tiltNormals,zoomNormals, 
										   width,height,   
										   tx,ty,tz,   
										   panOffset,tiltOffset, rollOffset, 
										   totalPanRad, totalTiltRad, 
										   focalF1, focalF2, focalF3)
	
	var error = 0;
	for (var i = 0; i < n; i++){
		var M = allM[i];
		error = error + getAngularErrorForCorrespondence(M, tx, ty, tz, u[i], v[i], x[i], y[i], z[i]);
	}
	
	return error;
}

function getAngularErrorForCorrespondence(M, tx, ty, tz, u, v, x, y, z){
	// from M and the image coordinates, find out which ray goes from the
	// camera center to the image through the user-provided image coordinates.
	
	// solving for XYZ:
	//   x*d = m11*X + m12*Y + m13*Z + m14
	//   y*d = m21*X + m22*Y + m23*Z + m24
	//   d = m31*X + m32*Y + m33*Z + m34
	//   (x*d - m14) = m11*X + m12*Y + m13*Z
	//   (y*d - m24) = m21*X + m22*Y + m23*Z 
	//   d - m34  = m31*X + m32*Y + m33*Z
	M = M.values;
	var w = -100;
	var mat = new Matrix(4,3, [[M[0][0], M[0][1], M[0][2], (u * w - M[0][3])], 
							   [M[1][0], M[1][1], M[1][2], (v * w - M[1][3])],
							   [M[2][0], M[2][1], M[2][2], ( w - M[2][3])]]
						 );
	var vals = mat.rowEchelon().values;
	var projectedRay = new Array(vals[0][3] - tx, vals[1][3] - ty, vals[2][3] - tz);
	
	var mag = Math.sqrt(projectedRay[0]*projectedRay[0] + 
						projectedRay[1]*projectedRay[1] + 
						projectedRay[2]*projectedRay[2])
	for (var j = 0; j < 3; j++){
		projectedRay[j] = projectedRay[j] / mag;
	}
	
	// now find the ray that goes from the camera center through the user-provided 3D XYZ coordinate.
	var rayXYZPoint = new Array(x - tx, y - ty, z - tz);
	var mag = Math.sqrt(rayXYZPoint[0]*rayXYZPoint[0] + 
						rayXYZPoint[1]*rayXYZPoint[1] +
						rayXYZPoint[2]*rayXYZPoint[2])
	for (var j = 0; j < 3; j++){
		rayXYZPoint[j] = rayXYZPoint[j] / mag;
	}
	//the error increases as the vectors become more and more opposite.
	// i.e., error = 1 - ray1 (dot) ray2.
	return 1 - (projectedRay[0] * rayXYZPoint[0] + 
			projectedRay[1] * rayXYZPoint[1] + 
			projectedRay[2] * rayXYZPoint[2]);
}



function ptzProjectionError(x,y,z,u,v,panNormals,tiltNormals,zoomNormals,width,height,sol,maxError){
	//	Returns the sum of all projection errors.
	//Input: x,y,z                              : positions in ENU space
	//	u,v                                : positions in image coordinates
	//	panNormals,tiltNormals,zoomNormals : pan, tilt, and zoom, in [0,1].
	//	width, height                      : the width and height of the image.
	//	sol                                : a 10-element solution vector, where
	//	sol[0:2]=tx,ty,tz               :    the position of the camera in ENU space.
	//	sol[3:5]=pan0,tilt0,roll0       :    the rotational offsets of the camera.
	//	sol[6:7]=focalMin,focalMax      :    the minimum and maximum focal lengths of the camera.
	//	sol[8:9]=dpan, dtilt            :    the total field of view of the camera in pan and tilt.
	//Output:  the pixel reprojection error.
	
	var n = x.length;
	var tx                = sol[0];
	var ty                = sol[1];
	var tz                = sol[2];
	var panOffset         = sol[3];
	var tiltOffset        = sol[4];
	//rollOffset        = sol[5]
	var rollOffset        = 0;
	var focalF1          = sol[5];
	var focalF2          = sol[6];
	var focalF3          = sol[7];
	var totalPanRad       = sol[8];
	var totalTiltRad      = sol[9];
	//var totalPanRad = 320.0 / 180.0 * Math.PI;
	//var totalTiltRad = 120.0 / 180.0 * Math.PI;


	//if (totalPanRad < 0 || totalTiltRad < 0 || totalPanRad > 2 * Math.PI || totalTiltRad > 2 * Math.PI)
	//	return Infinity;
	
	var allM = makeManyProjectionMatrices(panNormals,tiltNormals,zoomNormals, 
										  width,height,   
										  tx,ty,tz,   
										  panOffset,tiltOffset, rollOffset, 
										  totalPanRad, totalTiltRad, 
										  focalF1, focalF2,focalF3)
		
	var error = 0;
	for (var i = 0; i < n; i++){
		// calculate pixel reprojection error.
		var M = allM[i];
		var xyz = new Matrix(1,4, [[x[i]],[y[i]],[z[i]], [1]]);
		var uvw = M.multiply(xyz);
		uvw = uvw.values
		var projU = uvw[0][0] / uvw[2][0];
		var projV = uvw[1][0] / uvw[2][0];
		
		var dU = projU - u[i]
		var dV = projV - v[i]
		
		var d = Math.sqrt(dU*dU + dV*dV);
		if (d > maxError){
			d = maxError;
		}
		
		error = error + d
	}
	
	return error;
}